<?php
namespace hid\builders;

use Exception;
use \hid\base\IdBuilder;

/**
 * SnowflakeB 全局id生成器
 *<B>说明：</B>
 *<pre>
 *  id = 毫秒+数据中心标识+机器标识+自增
 * 1 sign bit -- 0 is positive, 1 is negative
 * 41 bits -- milliseconds since epoch
 * 5 bits -- dataCenter ID
 * 5 bits -- machine ID
 * 12 bits -- sequence number
 * Total 64 bit integer/string
 *</pre>
 */
class SnowflakeBuilder extends IdBuilder
{

    // 自定义纪元（起始时间戳），可根据实际情况调整
    protected $twepoch = 1288834974657;
    // 工作机器ID和数据中心ID（取值范围 0 ~ 最大值）
    protected $workerId;
    protected $datacenterId;

    // 各部分占用的位数
    protected $workerIdBits     = 5;   // 机器ID位数
    protected $datacenterIdBits = 5;   // 数据中心ID位数
    protected $sequenceBits     = 12;  // 序列号位数

    // 各部分的最大数值（通过位运算计算）
    protected $maxWorkerId;
    protected $maxDatacenterId;
    private $sequenceMask;

    // 各部分左移位数
    private $workerIdShift;
    private $datacenterIdShift;
    private $timestampLeftShift;

    // 序列号，记录同一毫秒内生成的ID序号
    private $sequence = 0;

    // 上一次生成ID的时间戳，初始化为 -1
    private $lastTimestamp = -1;

    /**
     * 构造函数
     */
    public function __construct(array $config = [])
    {
        parent::__construct($config);

        if (!is_null($this->workerId)) {
            $this->workerId = (int)$this->idManager->buildKey($this->workerId);
        }

        if (!is_null($this->datacenterId)) {
            $this->datacenterId = (int)$this->idManager->buildKey($this->datacenterId);
        }

        // 计算各部分最大值：如 -1 << 5 得到 0b11111 (31)
        $this->maxWorkerId     = -1 ^ (-1 << $this->workerIdBits);
        $this->maxDatacenterId = -1 ^ (-1 << $this->datacenterIdBits);
        // 序列号最大值（4095：12位全1）
        $this->sequenceMask    = -1 ^ (-1 << $this->sequenceBits);

        // 检查传入的机器ID和数据中心ID是否合法
        if ($this->workerId > $this->maxWorkerId || $this->workerId < 0) {
            throw new Exception("机器ID必须在 0 到 " . $this->maxWorkerId . " 之间");
        }
        if ($this->datacenterId > $this->maxDatacenterId || $this->datacenterId < 0) {
            throw new Exception("数据中心ID必须在 0 到 " . $this->maxDatacenterId . " 之间");
        }

        // 计算各部分需要左移的位数
        // 序列号占用 12 位，因此机器ID需要左移 12 位
        $this->workerIdShift     = $this->sequenceBits;
        // 数据中心ID需要左移 12 + 5 = 17 位
        $this->datacenterIdShift = $this->sequenceBits + $this->workerIdBits;
        // 时间戳需要左移 12 + 5 + 5 = 22 位
        $this->timestampLeftShift = $this->sequenceBits + $this->workerIdBits + $this->datacenterIdBits;
    }

    /**
     * 生成下一个唯一ID
     *
     * @return int 64位的唯一ID
     */
    public function getId():string
    {
        // 获取当前毫秒级时间戳
        $timestamp = $this->timeGen();

        // 如果当前时间小于上一次生成ID的时间戳，则说明系统时钟回拨，无法生成ID
        if ($timestamp < $this->lastTimestamp) {
            throw new Exception("时钟回拨异常，拒绝生成ID. 差值: " . ($this->lastTimestamp - $timestamp) . " 毫秒");
        }

        // 如果当前时间与上次生成ID时间相同，表示同一毫秒内生成多个ID
        if ($timestamp == $this->lastTimestamp) {
            // 序列号自增，并通过位运算确保在 12 位内（最大值为 4095）
            $this->sequence = ($this->sequence + 1) & $this->sequenceMask;
            // 如果序列号达到最大值，则等待下一毫秒
            if ($this->sequence == 0) {
                $timestamp = $this->tilNextMillis($this->lastTimestamp);
            }
        } else {
            // 不同毫秒内，序列号重置为 0
            $this->sequence = 0;
        }

        // 更新上一次生成ID的时间戳
        $this->lastTimestamp = $timestamp;

        /**
         * 拼接各部分生成最终ID：
         * 1. (当前时间戳 - 自定义纪元) 左移 TIMESTAMP_LEFT_SHIFT 位
         * 2. 数据中心ID左移 DATACENTER_ID_SHIFT 位
         * 3. 机器ID左移 WORKER_ID_SHIFT 位
         * 4. 序列号（低12位）
         * 最终通过按位或（|）运算组合成一个 64 位的整数
         */
        $id = (($timestamp - $this->twepoch) << $this->timestampLeftShift)
            | ($this->datacenterId << $this->datacenterIdShift)
            | ($this->workerId << $this->workerIdShift)
            | $this->sequence;

        return (string)$id;
    }

    /**
     * 等待直到下一毫秒
     *
     * @param int $lastTimestamp 上次生成ID的时间戳
     * @return int 下一毫秒的时间戳
     */
    private function tilNextMillis($lastTimestamp)
    {
        $timestamp = $this->timeGen();
        // 循环等待直到时间戳发生变化（进入下一毫秒）
        while ($timestamp <= $lastTimestamp) {
            $timestamp = $this->timeGen();
        }
        return $timestamp;
    }

    /**
     * 获取当前时间戳（毫秒级）
     *
     * @return int 当前毫秒级时间戳
     */
    private function timeGen()
    {
        // microtime(true) 返回秒为单位的小数，将其乘以 1000 转换为毫秒
        return (int) (microtime(true) * 1000);
    }


}
